IM 消息文件实现
July 23, 2016
背景:
之前负责的云盘业务和 内部使用的大象 IM 融合,云盘需要负责大象日常 im 所有的文件流转,这里涉及到两个现有系统融合的问题,大象 之前的协议里面文件消息包含的是一个静态的链接,目前还没有上鉴权。云盘设计之初就为了保证安全性,所有分发出去的链接都是动态的,有鉴权信息和时效的。那么如何设计一个有鉴权信息而且还满足大象 im 协议需求的 url 就成了融合里面稍微麻烦点的问题。
最初的想法
1,链接只包含必要的im id和cookie(或者header),然后请求到服务端,服务端鉴权后,定位到文件,然后重定向到一个动态链接上面。这个方案的问题在app端兼容上面,而且一次图片要发起两次请求,体验不好,第一个被否决。 2,使用java/go转发流量,链接包含必要的im id和和cookie(或者header),请求到一个java的服务,鉴权和定位文件没有问题,就去mss上面请求文件,然后使用流对流的方式返回给客户端,这个有两个问题: a:观察流量走向: 业务nginx->java服务->mss nginx->swift集群,这里有两个问题,如果扩容,那么java服务和swift集群都要同时扩容,流量在内部走了两份就代表流量浪费和相应时间加长 b:流对流不稳定,云盘在非核心的地方尝试使用过流对流的方案,基本上每3W 次请求会有1~2次失败,因为关键节点太多,稳定性是乘法关系,网络或者机器内存等等原因稍微有问题,就可能导致失败这不满足 im 图片在消息稳定性上的要求
进一步的想法
仔细想了下,重新观察正常的流量走向是:请求->mss nginx->swift 集群
。在方案 2 中 我们添加 java 服务的原因就是为了做:1,鉴权 2,文件寻址。
根据我之前的了解,其实可以在 nginx 上面直接做,所以进一步我想直接用 lua 或者 c 给 nginx 写这个业务模块,和基建的同学沟通后得知他们的 nginx 可以直接写 lua 拓展,那么事情就简单了
第一个直觉想法是流量请求到 nginx 里面,然后由 lua 鉴权,然后文件寻址。lua 请求到文件后添加到 nginx response content 里面核心代码大概就像下面这样
//前面是鉴权,寻址文件获得url
local http = require("socket.http");
local body, code = http.request("http://7xi576.com1.z0.glb.clouddn.com/01CE5B62-E4E2-4D84-99EC-BE70DA46269B.png")
if not body then ngx.say("获取不到文件");return; end
ngx.print(body);
但是转头一想,如果由 lua 去获取文件,肯定没有 nginx 本身 c 模块直接获取文件要好一些,所以有了接下来的想法,核心原理就是直接用 nginx 的 proxy_pass 特性,每次请求上下文中由 lua 鉴权,寻址文件,并动态动态替换需要 proxy_pass 的 url 和 content-type 之类的参数即可。 这样整个文件流转就变成下面比较合理的方式:
client request file -> mss nginx(鉴权,文件寻址,代理到文件)-> mss server
整个方案稳定性和云盘原有方案基本保持一致,安全性一致,同时兼容大象im协议。 核心 lua 代码大概是下面样子:
local http = require("socket.http");
local ltn12 = require("ltn12")
local cjson = require ("cjson");
local cjsonObj = cjson.new()
local var = ngx.var;
local request_body = [[ { "objectId" : "test","password" : "123" } ]];
local response_body = { }
local body, code, response_headers = http.request{
url = "http://127.0.0.1:8411/image",
method = "POST",
headers =
{
["Authorization"] = "Maybe you need an Authorization header?",
["Content-Type"] = "application/json",
["Content-Length"] = request_body:len(),
["Cookie"] = var.http_cookie,
},
source = ltn12.source.string(request_body),
sink = ltn12.sink.table(response_body)
}
if type(response_body) == "table" then
local resBodyStr = table.concat(response_body,"");
local resJsonObj = cjsonObj.decode(resBodyStr);
if code == 200 then
if not resJsonObj.error then
var.image_proxy_url = resJsonObj["filepath"];
else
ngx.say(resJsonObj.error.msg);
end
else
ngx.say("获取不到文件");
end
else
ngx.say("Not a table:", type(response_body))
end
进一步的想法
如果频繁(每次请求更改一次)更改的 proxy_pass 的话,对 nginx 的性能会有影响,同时模块见耦合也过于严重,so,我们的想法进一步变成通过内部 rewrite 来进行解耦,同时隔离开各个模块: auth,error,not-fount,proxy_pass. 整个流程说难也不难,主要是思路上的问题,同时还要对nginx模块/拓展开发足够熟悉并能灵活运用。
Written by xi ming You should follow him on Github